/*
 * (C) 2007-2012 Alibaba Group Holding Limited.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * Authors:
 *   wuhua <wq163@163.com> , boyan <killme2008@gmail.com>
 */
package com.taobao.metamorphosis.client.extension.log4j;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import org.apache.log4j.Layout;
import org.apache.log4j.helpers.LogLog;


/**
 * 
 * @author wuxin
 * @since 1.0, 2009-10-20 下午03:49:32
 */
public class FileStreamAppender extends StreamAppender {

    /**
     * Controls file truncatation. The default value for this variable is
     * <code>true</code>, meaning that by default a <code>FileAppender</code>
     * will append to an existing file and not truncate it.
     * 
     * <p>
     * This option is meaningful only if the FileAppender opens the file.
     */
    protected boolean fileAppend = true;

    /**
     * The name of the log file.
     */
    protected String fileName = null;

    /**
     * Do we do bufferedIO?
     */
    protected boolean bufferedIO = false;

    /**
     * Determines the size of IO buffer be. Default is 8K.
     */
    protected int bufferSize = 8 * 1024;


    /**
     * The default constructor does not do anything.
     */
    public FileStreamAppender() {
    }


    /**
     * Instantiate a <code>FileAppender</code> and open the file designated by
     * <code>filename</code>. The opened filename will become the output
     * destination for this appender.
     * 
     * <p>
     * If the <code>append</code> parameter is true, the file will be appended
     * to. Otherwise, the file designated by <code>filename</code> will be
     * truncated before being opened.
     * 
     * <p>
     * If the <code>bufferedIO</code> parameter is <code>true</code>, then
     * buffered IO will be used to write to the output file.
     */
    public FileStreamAppender(Layout layout, String filename, boolean append, boolean bufferedIO, int bufferSize)
            throws IOException {
        this.layout = layout;
        this.setFile(filename, append, bufferedIO, bufferSize);
    }


    /**
     * Instantiate a FileAppender and open the file designated by
     * <code>filename</code>. The opened filename will become the output
     * destination for this appender.
     * 
     * <p>
     * If the <code>append</code> parameter is true, the file will be appended
     * to. Otherwise, the file designated by <code>filename</code> will be
     * truncated before being opened.
     */
    public FileStreamAppender(Layout layout, String filename, boolean append) throws IOException {
        this.layout = layout;
        this.setFile(filename, append, false, bufferSize);
    }


    /**
     * Instantiate a FileAppender and open the file designated by
     * <code>filename</code>. The opened filename will become the output
     * destination for this appender.
     * 
     * <p>
     * The file will be appended to.
     */
    public FileStreamAppender(Layout layout, String filename) throws IOException {
        this(layout, filename, true);
    }


    /**
     * The <b>File</b> property takes a string value which should be the name of
     * the file to append to.
     * 
     * <p>
     * <font color="#DD0044"><b>Note that the special values "System.out" or
     * "System.err" are no longer honored.</b></font>
     * 
     * <p>
     * Note: Actual opening of the file is made when {@link #activateOptions} is
     * called, not when the options are set.
     */
    public synchronized void setFile(String file) {
        // Trim spaces from both ends. The users probably does not want
        // trailing spaces in file names.
        String val = file.trim();
        fileName = val;
    }


    /**
     * Returns the value of the <b>Append</b> option.
     */
    public boolean getAppend() {
        return fileAppend;
    }


    /** Returns the value of the <b>File</b> option. */
    public synchronized String getFile() {
        return fileName;
    }


    /**
     * If the value of <b>File</b> is not <code>null</code>, then
     * {@link #setFile} is called with the values of <b>File</b> and
     * <b>Append</b> properties.
     * 
     * @since 0.8.1
     */
    @Override
    public void activateOptions() {
        if (fileName != null) {
            try {
                setFile(fileName, fileAppend, bufferedIO, bufferSize);
            }
            catch (java.io.IOException e) {
                LogLog.error("setFile(" + fileName + "," + fileAppend + ") call failed.", e);
            }
        }
        else {
            // LogLog.error("File option not set for appender ["+name+"].");
            LogLog.warn("File option not set for appender [" + name + "].");
            LogLog.warn("Are you using FileAppender instead of ConsoleAppender?");
        }
    }


    /**
     * Closes the previously opened file.
     */
    protected void closeFile() {
        if (this.out != null) {
            try {
                this.out.close();
            }
            catch (java.io.IOException e) {
                // Exceptionally, it does not make sense to delegate to an
                // ErrorHandler. Since a closed appender is basically dead.
                LogLog.error("Could not close " + out, e);
            }
        }
    }


    /**
     * Get the value of the <b>BufferedIO</b> option.
     * 
     * <p>
     * BufferedIO will significatnly increase performance on heavily loaded
     * systems.
     */
    public boolean getBufferedIO() {
        return this.bufferedIO;
    }


    /**
     * Get the size of the IO buffer.
     */
    public int getBufferSize() {
        return this.bufferSize;
    }


    /**
     * The <b>Append</b> option takes a boolean value. It is set to
     * <code>true</code> by default. If true, then <code>File</code> will be
     * opened in append mode by {@link #setFile setFile} (see above). Otherwise,
     * {@link #setFile setFile} will open <code>File</code> in truncate mode.
     * 
     * <p>
     * Note: Actual opening of the file is made when {@link #activateOptions} is
     * called, not when the options are set.
     */
    public void setAppend(boolean flag) {
        fileAppend = flag;
    }


    /**
     * The <b>BufferedIO</b> option takes a boolean value. It is set to
     * <code>false</code> by default. If true, then <code>File</code> will be
     * opened and the resulting {@link java.io.Writer} wrapped around a
     * {@link BufferedWriter}.
     * 
     * BufferedIO will significatnly increase performance on heavily loaded
     * systems.
     */
    public void setBufferedIO(boolean bufferedIO) {
        this.bufferedIO = bufferedIO;
        if (bufferedIO) {
            immediateFlush = false;
        }
    }


    /**
     * Set the size of the IO buffer.
     */
    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }


    /**
     * <p>
     * Sets and <i>opens</i> the file where the log output will go. The
     * specified file must be writable.
     * 
     * <p>
     * If there was already an opened file, then the previous file is closed
     * first.
     * 
     * <p>
     * <b>Do not use this method directly. To configure a FileAppender or one of
     * its subclasses, set its properties one by one and then call
     * activateOptions.</b>
     * 
     * @param fileName
     *            The path to the log file.
     * @param append
     *            If true will append to fileName. Otherwise will truncate
     *            fileName.
     */
    public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
            throws IOException {
        LogLog.debug("setFile called: " + fileName + ", " + append);

        // It does not make sense to have immediate flush and bufferedIO.
        if (bufferedIO) {
            setImmediateFlush(false);
        }

        reset();
        FileOutputStream ostream = null;
        try {
            //
            // attempt to create file
            //
            ostream = new FileOutputStream(fileName, append);
        }
        catch (FileNotFoundException ex) {
            //
            // if parent directory does not exist then
            // attempt to create it and try to create file
            // see bug 9150
            //
            String parentName = new File(fileName).getParent();
            if (parentName != null) {
                File parentDir = new File(parentName);
                if (!parentDir.exists() && parentDir.mkdirs()) {
                    ostream = new FileOutputStream(fileName, append);
                }
                else {
                    throw ex;
                }
            }
            else {
                throw ex;
            }
        }

        if (bufferedIO) {
            this.out = new BufferedOutputStream(ostream, bufferSize);
        }
        else {
            this.out = ostream;
        }

        this.fileName = fileName;
        this.fileAppend = append;
        this.bufferedIO = bufferedIO;
        this.bufferSize = bufferSize;
        writeHeader();
        LogLog.debug("setFile ended");
    }


    /**
     * Close any previously opened file and call the parent's <code>reset</code>
     * .
     */
    @Override
    protected void reset() {
        closeFile();
        this.fileName = null;
        super.reset();
    }
}